Websocket

省略一万字

Socket.io

服务器端

客户端

React + socket.io 实现简易聊天室

我们由浅入深,先实现基础功能,再一步一步添加功能

第一步、React前端 与 服务器端建立链接

前言:

  • 简单理解socket.io中的onemit,相当于中央事件总线的onemiton表示接受对应事件,emit表示派发事件,两者是对应关系。
  • 服务端和客户端的链接是一对多的关系
  • 客户端 emit的事件只能被服务端的on接受,服务端的emit会被所有客户端的on接受
  • socket.io中的 connectdisconnect事件是默认emit

服务端

首先使用npm初始化一个项目,并在项目内安装 socket.io

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 服务端代码
const io = require('socket.io')(4000, {
// 对应客户端的path
path: '/socket.chat',
// serveClient 是否相应客户端请求
serveClient: true,
// transports 数组的前后顺序关系到socket创建实例,此处优先创建 ws 默认值为 ['polling', 'websocket']
transports: ['websocket', 'polling']
});

const connections = [];

io.sockets.on('connect', function (socket) {
connections.push(socket);
io.sockets.emit('userCount', { msg: connections.length })
console.log('connected >>> [ %s ] online, [ %s ]', connections.length, new Date())

socket.on('disconnect', function (data) {
connections.splice(connections.indexOf(socket), 1);
console.log('disconnect >>> [ %s ] online', connections.length);
delete socket;
})
})

console.log(`Server running on prot ${process.env.PORT || 4000}`);
1
2
// 启动
$ nodemon App.js

客户端

使用 create-react-app创建项目,并安装 socket.io

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 客户端代码
import React, { useMemo, useEffect } from 'react'
import styles from './index.module.scss'
import io from 'socket.io-client';
export default props => {
// 注意此处必须要使用useMemo来创建socket对象,
// 否则组件的State每改变一次,都会创建一个新的socket对象,而导致连接数不断增加
const socket = useMemo(() => {
// http://localhost:4000 对应的是服务器地址
return io('http://localhost:4000', {
// 对应服务器的path
path: '/socket.chat',
// 默认值为 [ 'polling' ]
transports: ['websocket']
});
}, [])

useEffect(() => {
socket.on('connect', () => {
socket.connected ? console.log('已连接...') : console.error('连接失败');
});
}, [socket])
return <div></div>
}
1
2
3
// 启动项目
$ npm start
// 输入localhost:3000

若连接成功,则显示

建立连接

浏览器新开一个标签页,后再输入 localhost:3000

建立链接2

若其他设备与本机在同一局域网下时,输入本机对应局域网 ip 地址加端口号,也可以连接到服务器而建立链接。

第二步、发送消息并在各设备间同步

服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const io = require('socket.io')(4000, {
path: '/socket.chat',
serveClient: true,
transports: ['websocket', 'polling']
});
const connections = [];

io.sockets.on('connect', function (socket) {
connections.push(socket);
console.log('connected >>> [ %s ] online, [ %s ]', connections.length, new Date())

socket.on('disconnect', function (data)
connections.splice(connections.indexOf(socket), 1);
console.log('disconnect >>> [ %s ] online', connections.length);
delete socket;
})

// 设置接收器
socket.on('sendMessage', function (data) {
console.log(data)
// 接收到消息后,再广播到各个设备
io.sockets.emit('newMessage', { msg: data })
})
})

console.log(`Server running on prot ${process.env.PORT || 4000}`);

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import React, { useState, useMemo, useCallback, useEffect } from 'react'
import styles from './index.module.scss'
import io from 'socket.io-client';
export default props => {
const [sendMessage, setSendMessage] = useState('');
const [receiveMessage, setReceiveMessage] = useState([]);

const socket = useMemo(() => {
return io('http://localhost:4000', {
path: '/socket.chat',
transports: ['websocket']
});
}, [])

useEffect(() => {
socket.on('connect', () => {
socket.connected ? console.log('已连接...') : console.error('连接失败');
});


socket.on('disconnect', () => {
socket.disconnected ? console.log('已断开链接...') : console.error('断开失败');
});

socket.on('newMessage', ({ msg }) => {
setReceiveMessage(state => ([
...state,
msg
]))
});
}, [socket])

//
const sendMsg = useCallback((msg) => {
socket.emit('sendMessage', { data: msg });
}, [socket])


return <div className={styles.wrap}>
<div className={styles.chatBox}>
{receiveMessage.length > 0 && receiveMessage.map(item => <p key={String(Date() + Math.random())}>{item.data}</p>)}
</div>
<div className={styles.chatInput}>
<div className={styles.chatInputLeft}></div>
<div className={styles.chatInputMid}>
{/* 将原生 `input`改造成受控组件 */}
<input type="text" className={styles.inputText} value={sendMessage} onChange={(e) => { setSendMessage(e.target.value) }} placeholder="请输入..." />
</div>
<button className={styles.chatInputRight} onClick={() => { sendMsg(sendMessage); setSendMessage(''); }}>发送</button>
</div>
</div >
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// style
.wrap {
height: 100%;
background-color: skyblue;
display: flex;
flex-direction: column;
position: relative;
font-size: 14px;
.chatBox {
background-color: skyblue;
}
.chatInput {
width: 100%;
height: 35px;
position: absolute;
bottom: 0;
left: 0;
display: flex;
&Left {
width: 50px;
height: 100%;
}
&Mid {
flex: 1;
display: flex;
align-items: center;
background-color: #fff;
.inputText {
width: 100%;
height: 90%;
border: 1px solid #eee;
padding: 0px 10px;
line-height: 35px;
}
}
&Right {
width: 50px;
height: 100%;
text-align: center;
line-height: 35px;
background-color: rgb(62, 139, 253);
border-radius: 10px;
color: #fff;
}
}
}

第三步、添加断开连接功能,各设备之间同步链接数

服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const io = require('socket.io')(4000, {
path: '/socket.chat',
serveClient: true,
transports: ['websocket', 'polling']
});

const connections = [];

io.sockets.on('connect', function (socket) {
connections.push(socket);
io.sockets.emit('userCount', { msg: connections.length })
console.log('connected >>> [ %s ] online, [ %s ]', connections.length, new Date())

socket.on('disconnect', function (data) {
connections.splice(connections.indexOf(socket), 1);
io.sockets.emit('userCount', { msg: connections.length});
console.log(connections.length)
console.log('disconnect >>> [ %s ] online', connections.length);
delete socket;
})

socket.on('sendMessage', function (data) {
console.log(data)
io.sockets.emit('newMessage', { msg: data })
})
})


console.log(`Server running on prot ${process.env.PORT || 4000}`);

客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
import React, { useState, useMemo, useCallback, useEffect } from 'react'
import styles from './index.module.scss'
import io from 'socket.io-client';
import { Link } from 'react-router-dom'
export default props => {

const [userCount, setUserCount] = useState(0)
const [sendMessage, setSendMessage] = useState('');
const [receiveMessage, setReceiveMessage] = useState([]);

const socket = useMemo(() => {
return io('http://localhost:4000', {
path: '/socket.chat',
// 默认值为 [ 'polling' ]
transports: ['websocket']
});
}, [])

useEffect(() => {
socket.on('connect', () => {
socket.connected ? console.log('已连接...') : console.error('连接失败');
});


socket.on('disconnect', () => {
socket.disconnected ? console.log('已断开链接...') : console.error('断开失败');
});

socket.on('userCount', ({ msg }) => {
console.log("%s 人在线", msg); // true
setUserCount(msg)
});

socket.on('newMessage', ({ msg }) => {
setReceiveMessage(state => ([
...state,
msg
]))
});
}, [socket])


// 断开链接申请
const emitDisconnect = useCallback((msg) => {
console.log('提交断开连接申请')
socket.close();
}, [socket])


const sendMsg = useCallback((msg) => {
socket.emit('sendMessage', { data: msg });
}, [socket])


return <div className={styles.wrap}>
<div className={styles.chatBox}>
<Link to="/home" >chat</Link>
<div>{userCount} 人在线</div>
{receiveMessage.length > 0 && receiveMessage.map(item => <p key={String(Date() + Math.random())}>{item.data}</p>)}
</div>
<button onClick={() => { emitDisconnect('wwww') }}>注销</button>
<div className={styles.chatInput}>
<div className={styles.chatInputLeft}></div>
<div className={styles.chatInputMid}>
<input type="text" className={styles.inputText} value={sendMessage} onChange={(e) => { setSendMessage(e.target.value) }} placeholder="请输入..." />
</div>
<button className={styles.chatInputRight} onClick={() => { sendMsg(sendMessage); setSendMessage(''); }}>发送</button>
</div>
</div >
}

以上三步的完成,可以基本实现一个聊天室的功能了,但是这远远不够。

接下来会增加更高级的功能,但为了文章简洁,将只会贴出关键性代码。

待更新

  1. 用户登陆校验
  2. 客户端分组

思维扩展

除了可以实现聊天室,也可以实现在线下棋对战等实时对战游戏。

× 请我吃糖~
打赏二维码